package controller;

import java.io.BufferedWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.StringTokenizer;
import org.apache.log4j.Logger;
import ssh.SSHCommandException;
import ssh.SSHConnectionHandler;
import utils.Command;
import utils.IllegalValueException;
import model.Bandwidth;
import model.DistributedCommand;
import model.Hub;
import model.Node;
import model.Hub.CommandType;
import model.IPAddress;
import model.Property;
import model.exception.TcException;
import model.exception.PathEmulationException;

public class PathEmulation {
	
	private ArrayList<Hub> hubs;
	/*File per operazioni di Undo*/
	private File file;
	
	private static final Logger log = Logger.getLogger(PathEmulation.class);
	
	/**
	 * Costruisce un oggetto TrafficControl con associata un {@link Hub}. Su tale oggetto è possibile
	 * eseguire i metodi {@link PathEmulation#enforce()}, {@link PathEmulation#destroy()} e {@link PathEmulation#resetTrafficControl()} 
	 * che avranno effetto su tutti i server fisici coninvolti nella topologia determinata dall'hub.  
	 * 
	 * @param hub - Hub della topologia di rete
	 */
	public PathEmulation(Hub hub){
		hubs = new ArrayList<Hub>();
		this.hubs.add(hub);
		file = new File("src/undo.txt");
		log.debug("Create file src/undo.txt to Undo Operation");
	}
	
	/**
	 * Costruisce un oggetto TrafficControl con associata una {@link ArrayList<{@link Hub}>. Su tale 
	 * oggetto è possibile eseguire i metodi {@link PathEmulation#enforce()}, 
	 * {@link PathEmulation#destroy()} e {@link PathEmulation#resetTrafficControl()} 
	 * che avranno effetto su tutti i server fisici coninvolti nella topologia determinata dalla lista di hub. 
	 * 
	 * @param hubs - Lista di Hub della topologia di rete
	 */
	public PathEmulation(ArrayList<Hub> hubs){
		this.hubs = hubs;
		file = new File("src/undo.txt");
		log.debug("Create file src/undo.txt to Undo Operation");
	}
	
	/**
	 * Crea le regole di Traffic Control descritte nell'oggetto {@link Property} e in quello {@link Bandwidth} .
	 * per tutte le VMs connesse, tramite link, all'Hub. I comandi tc vengono inviati ai server sui quali sono 
	 * allocate le VMs. Se solo un comando non viene eseguito correttamente viene generata una TrafficControlException.
	 * 
	 * @throws IOException
	 * @throws SSHCommandException
	 * @throws PathEmulationException 
	 * @throws IllegalValueException 
	 * @throws TcException 
	 */
	public void enforce() throws IOException, SSHCommandException,
											PathEmulationException, IllegalValueException, TcException{
		
		
		log.info("Enforce Traffic Control...");
		
		FileWriter w = new FileWriter(file);
		BufferedWriter bw=new BufferedWriter (w);
		
		for (Hub hub : hubs){
			
			log.info("Try to Enforce Traffic Control on Hub name: "+ hub.getName() +" ...");
			
			ArrayList<DistributedCommand> distributedCommands = new ArrayList<DistributedCommand>();
			
			distributedCommands = hub.getTrafficControlRules(CommandType.add);
			
			for(DistributedCommand command : distributedCommands)
			{			
				SSHConnectionHandler handler = SSHConnectionHandler.createSSHConnectionHandler(
													command.getPhysicalNode().getControlIP(),
													command.getPhysicalNode().getControlPort(),
													"root","nepadmpwd");
				/* 
				 * I comandi da eseguire vengono scritti nel file undo.txt prima che essi vengano
				 * eseguiti poichè in caso contrario un crash tra l'esecuzione del comando e la 
				 * scrittura nel log lascerebbe i dati in uno stato inconsistente. Bisogna però 
				 * ricordarsi, nella procedura undo(), che l'ultimo comando, avendo generato il codice
				 * di errore, non è stato eseguito sul server e quindi non va invertito.
				 */
				bw.write(command.getPhysicalNode().getControlIP() + " " +
	    				command.getPhysicalNode().getControlPort() + " " + "root nepadmpwd\n");
				/*
				 * A qdisc can be removed by specifying its handle, which may also be 'root'. All subclasses 
				 * and their leaf qdiscs are automatically deleted, as well as any filters attached to them.
				 */ 
				String deleteCommand = command.getCommand().substring(0,42).replaceAll("tc qdisc add dev","tc qdisc del dev");
				bw.write(deleteCommand + "\n");

				bw.flush();
				int exitCode = handler.sendCommand(command.getCommand());
				
				/*
				 * Nel caso in cui si ottiene un codice di errore 2 con StdErr "File exists",
				 * significa che esiste già una regola per quell'interfaccia. Poichè potrebbe
				 * avere parametri differenti essa viene eliminata e poi riscritta.
				 */
			    if(exitCode == 2 && handler.getStdErrString().indexOf("File exists")!=-1)
			    {	
			    	log.info("An old rule exists on interface: "+ deleteCommand.substring(17, 24));
			    	if(handler.sendCommand(deleteCommand)!=0){
			    		log.error("Can not delete old rule on interface: " 
			    				+ deleteCommand.substring(17, 24));
			    		throw new PathEmulationException("Can not delete old rule on interface: "
			    				+ deleteCommand.substring(17, 24));
			    	}
			    	log.info("Old rule updated on interface: "+ deleteCommand.substring(17, 24));
			    	exitCode = handler.sendCommand(command.getCommand());
			    }
			    
			    if(exitCode != 0){
					bw.close();
					w.close();
					
					log.error("Error executing command. Standard Error is >>> "+ handler.getStdErrString());
					undo();
					file.delete();
					return;
				}
			    
			}//fine ciclo su lista distributedCommands
		
			log.debug("...traffic Control enforce on Hub name: "+ hub.getName() +"!");
		}//fine ciclo su lista hubs
		
		bw.close();
		w.close();
		file.delete();
		
		log.info("Traffic Control enforced!");
	}
	
	public void replace() throws IOException, SSHCommandException,
														PathEmulationException, IllegalValueException{
		log.info("Replace Traffic Control");
		
		for (Hub hub : hubs){
			
			log.debug("Try to Update Traffic Control on Hub name: "+ hub.getName() +" ...");
						
			
			ArrayList<DistributedCommand> distributedCommands = new ArrayList<DistributedCommand>();
			try {
				distributedCommands = hub.getTrafficControlRules(CommandType.replace);
			} catch (TcException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
			
			for(DistributedCommand command : distributedCommands)
			{			
				SSHConnectionHandler handler = SSHConnectionHandler.createSSHConnectionHandler(
													command.getPhysicalNode().getControlIP(),
													command.getPhysicalNode().getControlPort(),
													"root","nepadmpwd");
				int exitCode = handler.sendCommand(command.getCommand());
				
			    if(exitCode != 0){
					
			    	log.error("Error executing command. Standard Error is >>> "+ handler.getStdErrString());

					return;
				}
			    
			}//fine ciclo su lista distributedCommands
		
		}//fine ciclo su lista hubs
		
		log.info("Traffic Control replaces!");
	}
	
	public void destroy() throws IOException, SSHCommandException,PathEmulationException{
		log.info("Destroy Traffic Control");
		
		for (Hub hub : hubs){
			
			log.debug("Try to Destroy Traffic Control on Hub name: "+ hub.getName());
			
			ArrayList<DistributedCommand> distributedCommands = new ArrayList<DistributedCommand>();
			try {
				distributedCommands = hub.getTrafficControlRules(CommandType.del);
			} catch (TcException e) {
				e.printStackTrace();
			}
			
			for(DistributedCommand command : distributedCommands)
			{			
				SSHConnectionHandler handler = SSHConnectionHandler.createSSHConnectionHandler(
													command.getPhysicalNode().getControlIP(),
													command.getPhysicalNode().getControlPort(),
													"root","nepadmpwd");
				int exitCode = handler.sendCommand(command.getCommand());
				if(exitCode!=0 && exitCode!=2  ){ 
					throw new PathEmulationException();
				}
			}
		
		}//fine ciclo for su hubs
		
		log.info("Traffic Control Destroyed!");
	}
	
	
	/**
	 * Reset di tutte le discipline di accodamento delle interfacce su ogni server.
	 * 
	 * @throws PathEmulationException
	 * @throws SSHCommandException
	 * @throws IOException
	 */
	public void reset() throws PathEmulationException, SSHCommandException, IOException {
		
		ArrayList<Node> nodes = this.getPhysicalNode();
		
		for(Node node : nodes)
		{		
			SSHConnectionHandler handler;
			handler = SSHConnectionHandler.createSSHConnectionHandler(
										node.getControlIP(),node.getControlPort(),
										"root", "nepadmpwd");
			
			if(handler.sendCommand(Command.getTratticControlQdiscShow())==0)
			{
				String stdOut = handler.getStdOutString();

				String[] split = stdOut.split(System.getProperty("line.separator"));
				for (int i = 0; i < (split.length);i++){
	        		
					StringTokenizer st = new StringTokenizer(split[i]);
					st.nextToken();st.nextToken();st.nextToken();st.nextToken();
					
					int exitCode = handler.sendCommand(
							Command.getTratticControlQdiscRESET().replaceFirst("DEV",st.nextToken()));
					if (exitCode!=0 && (exitCode!=2) ){
						log.error("Cannot reset Traffic Control");
						throw new PathEmulationException();
					}
				}//fine ciclo for
			}else
			{
				log.error("Cannot reset Traffic Control");
				new PathEmulationException();
			}
			
			handler.dispose();
		}
	}
	
	/**
	 * Restituisce la lista di tutti i nodi Fisici sui quali sono istanziate le VM con almeno una 
	 * scheda di rete virtuale connessa, tramite un link, alla lista di Hub.
	 * 
	 * @return {@link ArrayList<{@link Node}>
	 */
	private ArrayList<Node> getPhysicalNode(){
	
		ArrayList<Node> physicalNodes = new ArrayList<Node>();
		
		for (Hub hub : hubs){
			
			ArrayList<Node> tmp  = hub.getPhysicalNode();
			
			for (Node node : tmp){
				if ( physicalNodes.indexOf(node) == -1 )
					physicalNodes.add(node);
			}
		}
		return physicalNodes;
	}
	
	
	/**
	 * 
	 * 
	 * @throws IOException
	 * @throws SSHCommandException
	 * @throws PathEmulationException
	 */
	private void undo() throws IOException, SSHCommandException, 
										PathEmulationException{
		
		log.info("Start procedure UNDO");
        FileInputStream fis = new FileInputStream(file);

        byte[] content = new byte[fis.available()];
        fis.read(content);
         
        String data = new String(content);
        String[] split = data.split(System.getProperty("line.separator"));
        
        if((split.length % 2)!=0)
        {
        	log.error("Wrong number of entries in the file undo.txt!");
        	throw new PathEmulationException("Wrong number of entries in the file undo.txt!");
        }
        else if ((split.length-2) > 0){
        	
        	boolean isCommand = true;
        	String command = "";
        	for (int i = (split.length-2);i>0;i--){
        		
        		if(isCommand)
        		{
        			command = split[i-1];
        			isCommand = false;
        		}else
        		{		
        			StringTokenizer st = new StringTokenizer(split[i-1]);
        			SSHConnectionHandler handler;
					try {
						handler = SSHConnectionHandler.createSSHConnectionHandler(
											new IPAddress(4,st.nextToken(),"255.255.255.0"),
											Integer.parseInt(st.nextToken()),
											st.nextToken(),
											st.nextToken());
					} catch (NumberFormatException e) {
						e.printStackTrace();
						throw new PathEmulationException();
					} catch (IllegalValueException e) {
						e.printStackTrace();
						throw new PathEmulationException();
					}
        			
        			if( handler.sendCommand(command) != 0 ){
        				log.error("...UNDO procedure failure!");
        				throw new PathEmulationException(handler.getStdErrString());
        			}
        			
        			isCommand = true;        			
        		}
        		
        	}//fine ciclo for
        	
        }else{
        	log.warn("...no rule included and therefore no rule to be restored!");
        	return;
        }
        log.info("UNDO procedure completed successfully!");
	}
	
	
}
